package view;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;


/**
 * A visualization that takes in a single DataVector and finds the frequency
 * with which its elements fall within a chosen number of equally sized ranges.
 * 
 * @author Grant Oakley
 *         modified by Zachary Hopping
 * 
 */
@SuppressWarnings("serial")
public class Histogram extends Visualization {

    private static final int DEFAULT_NUMBER_OF_BINS = 20;
    private static final Color DEFAULT_FILL_COLOR = Color.BLUE;

    private int myNumberOfBins = DEFAULT_NUMBER_OF_BINS;
    private ArrayList<Integer> myBins;

    private static final String NOT_ENOUGH_DATA_ERROR_MESSAGE = "Not enough data was provided";
    private static final String DATA_INDEX_OUT_OF_BOUNDS_ERROR_MESSAGE = "Data index out of bounds";

    /**
     * Default constructor.
     */
    public Histogram () {
        this(DEFAULT_NUMBER_OF_BINS);
    }

    /**
     * Makes a histogram with the specified preferred size.
     * 
     * @param size sets the preferred size of the component
     */
    public Histogram (Dimension size) {
        this(size, DEFAULT_NUMBER_OF_BINS);
    }

    /**
     * Makes a histogram with the specified number of bins (partitions of data).
     * 
     * @param numberOfBins number of bins to divide the data into
     */
    public Histogram (int numberOfBins) {
        super();
        myNumberOfBins = numberOfBins;
        setColor(DEFAULT_FILL_COLOR);
    }

    /**
     * Makes a histogram with the specified preferred size and number of bins
     * (partitions of data).
     * 
     * @param size sets the preferred size of the component
     * @param numberOfBins number of bins to divide the data into
     */
    public Histogram (Dimension size, int numberOfBins) {
        super(size);
        myNumberOfBins = numberOfBins;
        setColor(DEFAULT_FILL_COLOR);
    }

    @Override
    /**
     * Paints the histogram onto our canvas.  Updates current bins and generates relative bin heights.
     * 
     * @param g provides a graphics canvas to paint on
     */
    public void paint (Graphics g) {
        Graphics2D g2d = (Graphics2D) g;
        updateBins();

        float[] binRelativeXPositions = findBinRelativeXPositions();

        float[] binRelativeHeights = findBinRelativeHeights();

        paintBins(g2d, binRelativeXPositions, binRelativeHeights);
        paint2DAxisAndLabels(g2d);
    }

    private float[] findBinRelativeXPositions () {
        float[] binRelativeXPositions = new float[myNumberOfBins + 1];
        for (int i = 0; i < binRelativeXPositions.length; i++) {
            binRelativeXPositions[i] =
                    mapToRange(i, 0, binRelativeXPositions.length - 1, getPlotBoundaryLeft(),
                               getPlotBoundaryRight());
        }
        return binRelativeXPositions;
    }

    private float[] findBinRelativeHeights () {
        int minimumBinSize = Collections.min(myBins);
        int maximumBinSize = Collections.max(myBins);
        float[] binRelativeHeights = new float[myBins.size()];
        for (int i = 0; i < binRelativeHeights.length; i++) {
            if (myBins.get(i) == 0) {
                binRelativeHeights[i] = getPlotBoundaryBottom();
            }
            else {
                binRelativeHeights[i] =
                        mapToRange(myBins.get(i), minimumBinSize, maximumBinSize,
                                   getPlotBoundaryBottom(), getPlotBoundaryTop());
            }
        }
        return binRelativeHeights;
    }

    /**
     * Determines what the values of the partitions between bins, and
     * distributes the data into these partitions.
     */
    private void updateBins () {
        myBins = new ArrayList<Integer>(myNumberOfBins);
        for (int i = 0; i < myNumberOfBins; i++) {
            myBins.add(i, 0);
        }

        DataVector<? extends Comparable<?>> data = null;
        try {
            data = getAxis(0);
            if (data.size() <= 1) { throw new ViewException(NOT_ENOUGH_DATA_ERROR_MESSAGE, data); }
        }
        catch (IndexOutOfBoundsException e) {
            e.printStackTrace();
            throw new ViewException(DATA_INDEX_OUT_OF_BOUNDS_ERROR_MESSAGE, data);
        }

        data.sort();
        float[] valueAtBinPartitions = new float[myNumberOfBins];
        float[] binPartitionRelativeLocations = new float[myNumberOfBins];
        float dataMinimum = data.getRangeLowerBound();
        float dataMaximum = data.getRangeUpperBound();

        setBinPartitions(valueAtBinPartitions, binPartitionRelativeLocations, dataMinimum,
                         dataMaximum);
        distributeDataIntoBins(data, valueAtBinPartitions);
    }

    private void distributeDataIntoBins (DataVector<? extends Comparable<?>> data,
                                         float[] valueAtBinPartitions) {
        int j = 0;
        for (int i = 0; i < myNumberOfBins; i++) {
            while (data.getAsFloat(j) <= valueAtBinPartitions[i]) {
                myBins.set(i, myBins.get(i) + 1);
                j++;
            }
        }
    }

    private void setBinPartitions (float[] valueAtBinPartitions,
                                   float[] binPartitionRelativeLocations, float dataMin,
                                   float dataMax) {
        for (int i = 0; i < myNumberOfBins; i++) {
            valueAtBinPartitions[i] =
                    mapToRange((float) i + 1, 0, myNumberOfBins + 1, dataMin, dataMax);

            binPartitionRelativeLocations[i] =
                    mapToRange((float) i + 1, 0, myNumberOfBins + 1, getPlotBoundaryLeft(),
                               getPlotBoundaryRight());
        }
    }

    private void paintBins (Graphics2D g2d, float[] binRelativeXPositions,
                            float[] binRelativeHeights) {
        for (int i = 0; i < binRelativeXPositions.length - 1; i++) {
            int leftSide = (int) (binRelativeXPositions[i] * getSize().width);
            int rightSide = (int) (binRelativeXPositions[i + 1] * getSize().width);
            int binTop = (int) (binRelativeHeights[i] * getSize().height);
            int binHeight = ((int) (getPlotBoundaryBottom() * getSize().height)) - binTop;
            g2d.setColor(getColor());
            g2d.fillRect(leftSide, binTop, rightSide - leftSide, binHeight);
            g2d.setColor(Color.BLACK);
            g2d.drawRect(leftSide, binTop, rightSide - leftSide, binHeight);
        }
    }

    @Override
    public boolean isCorrectNumberOfDataVectors (int numberOfVectors) {
        return numberOfVectors == 1;
    }

    @Override
    protected boolean dataVectorDimensionAgreement (List<?>[] vectors) {
        return true;
    }

}
